/*
 * FractalGenerator.java
 * 
 */

package matchstiklib;


import java.lang.reflect.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import java.awt.image.*;

/**
 * Generates the fractal image data using an algorithm based on flame
 * 
 * @author Jon Ludwig
 */
public class FractalGenerator {

    private BufferedImage image;
    private double xrange, yrange;
    private int w, h;
    private int iters;
    private int[][] bins;
    private int[][] r;
    private int[][] g;
    private int[][] b;
    //private float[][] c;
    private double max;
    private boolean canceled;
    
    /*
    private final float BLURW = 18;
    private final float[] blurkernl = {1.0f/BLURW, 1.0f/BLURW, 1.0f/BLURW, 
                                       1.0f/BLURW, 10.0f/BLURW, 1.0f/BLURW,
                                       1.0f/BLURW, 1.0f/BLURW, 1.0f/BLURW};
    */
    
    public FractalGenerator(int iterations) {
        iters = iterations;
    }
    
    public void Generate(Fractal f)
    {
    	
    	w = f.width;
    	h = f.height;
    	xrange = f.xrange;
    	yrange = f.yrange;
    	
    	FunctionModel[] funcs = f.funcs.toArray(new FunctionModel[0]);
    	
        int i;
        int[] fbins = new int[funcs.length];
        //double[] fdist = {0.01, 0.07, 0.07, 0.85};
        
        bins = new int[w][h];
        r = new int[w][h];
        g = new int[w][h];
        b = new int[w][h];
        //c = new float[w][h];
        max = 0;
        int count = 0;
        
        float color;
        int rgb;
        color = funcs[0].color; //(float);Math.random();
        
        FPoint p = FPoint.Random(xrange, yrange);
        int x,y;
        
        //System.out.println("max: " + Integer.MAX_VALUE + ", iters: " + iters);
        System.out.println("seed: " + p.x + ", " + p.y);
        
        canceled = false;
        
        for (int k = 0; k < iters && !canceled; k++)
        {
            i = RandomFunc(funcs);
            fbins[i]++;
            p = InvokeFunction(p, funcs[i]);
            color = (color + funcs[i].color)/2.0f;
            
            if (k >= 20)
            {
                x = p.scaleX(w);
                y = p.scaleY(h);
                
                if ((x >= 0) && (x < w) && (y >= 0) && (y < h))
                {
                    count++;
                    bins[x][y]++;
                    //c[x][y] += color;
                    rgb = f.palette[(int)(color*255)]; //palette.getRGB((int)(color*255), 0);
                    r[x][y] += (rgb >> 16) & 0xFF;
                    g[x][y] += (rgb >> 8) & 0xFF;
                    b[x][y] += rgb & 0xFF;
                    
                    if (bins[x][y] > max) max = bins[x][y];
                }
            }
        }
        
        System.out.println(count + " points plotted");
        for (int j = 0; j < fbins.length; j++)
            System.out.println("function #" + j + ": " + fbins[j] + " calls");
    }
    
    public BufferedImage Render(double gamma, boolean perform_blur, float sharpness, int blurtimes)
    {        
        double alpha;
        //int rgb, r, g, b;
        double _r, _g, _b, _a;
        double scalealpha;
        double altgamma;
        
        image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        DataBufferInt dbi = (DataBufferInt)image.getRaster().getDataBuffer();
        int[] buff = dbi.getData();
        
        double logmax = Math.log(max+1);
        
        canceled = false;
        
        for (int x = 0; x < w && !canceled; x++)
        {
            for (int y = 0; y < h && !canceled; y++)
            {
                if (bins[x][y] > 0)
                {
                    /*
                    val = Math.log(bins[x][y]+1);
                    val /= max;
                    val *= 255;
                    if (val > 255) System.out.println("truble");
                    //image.setRGB(x, y, Color.red.getRGB());
                    buff[x+(y*w)] = (255 << 24 | (int)val << 16 | (int)val << 8 | (int)val);
                    //Color.HSBtoRGB((float)val + hueshift, 1.0f, 1.0f);
                    */

                    alpha = Math.log(bins[x][y]+1)/bins[x][y];
                    scalealpha = (alpha / logmax) * bins[x][y];
                    altgamma = gamma;
                    
                    //_r = Math.pow(((r[x][y]/255 * alpha) / logmax), altgamma) * 255;
                    //_g = Math.pow(((g[x][y]/255 * alpha) / logmax), altgamma) * 255;
                    //_b = Math.pow(((b[x][y]/255 * alpha) / logmax), altgamma) * 255;
                    _r = (double)r[x][y]/bins[x][y]/255;
                    _g = (double)g[x][y]/bins[x][y]/255;
                    _b = (double)b[x][y]/bins[x][y]/255;
                    _r = Math.pow(_r, Math.pow(_r, 3)*(-gamma+1)+1) * 255;
                    _g = Math.pow(_g, Math.pow(_g, 3)*(-gamma+1)+1) * 255;
                    _b = Math.pow(_b, Math.pow(_b, 3)*(-gamma+1)+1) * 255;
                    _a = Math.pow(((bins[x][y] * alpha) / logmax), altgamma) * 255;
                    
                    /*
                    _r = Math.pow(r[x][y]/255 / max, gamma) * alpha / logmax * bins[x][y] * 255;
                    _g = Math.pow(g[x][y]/255 / max, gamma) * alpha / logmax * bins[x][y] * 255;
                    _b = Math.pow(b[x][y]/255 / max, gamma) * alpha / logmax * bins[x][y] * 255;
                     * */

                    if (_r > 255 || _g > 255 || _b > 255) System.out.println("truble");
                    buff[x+(y*w)] = ((int)_a << 24) | (int)_r << 16 | (int)_g << 8 | (int)_b;

                    //rgb = palette.getRGB((int)(((c[x][y]*alpha)/logmax) * 255), 0);
                    //buff[x + (y*w)] = 255 << 24 | rgb;
                }
                else
                    buff[x + (y*w)] = 0; // 255 << 24; // background
            }
        }
        
        BufferedImage finalimage = image;
        
        if (perform_blur)
        {
            float[] blurkernl = new float[9];
            for (int i = 0; i < 9; i++) blurkernl[i] = 1.0f;
            blurkernl[4] = sharpness;
            
            float gweight = 8.0f + sharpness;
            for (int i = 0; i < 9; i++) blurkernl[i] = blurkernl[i] / gweight;
            
            Kernel blur = new Kernel(3, 3, blurkernl);
            ConvolveOp gaussian = new ConvolveOp(blur);
            
            finalimage = image;
            
            for (int i = 0; i < blurtimes; i++)
                finalimage = gaussian.filter(finalimage, null); // use alpha or not?
        }
                
        return finalimage;
    }
    
    public void cancel()
    {
        canceled = true;
    }
    
    public static int RandomFunc(FunctionModel[] funcs)
    {
        int i;
        
        double[] ranges = new double[funcs.length-1];
        double last = 0.0;
        for (int j = 0; j < funcs.length-1; j++)
        {
            last = last + funcs[j].weight;
            ranges[j] = last;
        }
        
        double r = Math.random();
        i = funcs.length-1;
        for (int j = 0; j < funcs.length-1; j++)
        {
            if (r < ranges[j])
            {
                i = j;
                break;
            }
        }
        
        return i;
    }
    
    public static FPoint InvokeFunction(FPoint p, FunctionModel func)
    {
        try {
            Class<FunctionLibrary> c = FunctionLibrary.class;
            Class[] paramtypes = {FPoint.class};
            Method m = c.getMethod(func.name, paramtypes);
            
            // linear transform
            p.x = (func.a * p.x) + (func.b * p.y) + func.c;
            p.y = (func.d * p.x) + (func.e * p.y) + func.f;
            Object[] args = {p};
           
            p = (FPoint)m.invoke(null, args);
            
        } catch (NoSuchMethodException ex) {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        } catch (InvocationTargetException ex) {
            Logger.getLogger("global").log(Level.SEVERE, null, ex);
        }
        
        return p;
    }
}
